#include "hOLED.h"
#include "Delay.h"

/*----------------- OLED屏幕的硬件寻址字节 -----------------*/
#define OLED_ADDRESS_WRITE 0x78   // 写模式的OLED硬件的寻址字节
#define OLED_ADDRESS_READ  0x79   // 读模式的OLED硬件的寻址字节
#define UART_SCL_PIN   GPIO_Pin_10  	// 时钟线
#define UART_SDA_PIN   GPIO_Pin_11   	// 数据线
#define UART_GPIO_PORT	GPIOB

/**
  * @brief 初始化OLED以及I2C外设
  * @param  无
  * @param  无
  * @retval 无
  */
void hOLED_Init(void) {
	// 1.开启 GPIOB 时钟 | 初始化 SCL 和 SDA 为复用开漏输出
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);		// 开启GPIOB时钟
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Pin = UART_SCL_PIN | UART_SDA_PIN;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_OD;				// 复用开漏输出模式
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz;				// 2MHz引脚输出速度足够
	GPIO_Init(UART_GPIO_PORT, &GPIO_InitStruct);
	
	// 2.复位I2C2外设
	// - 初始化I2C2外设之前，先复位此外设，以清楚上一次I2C通信导致的错误
	RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C2, ENABLE);		// 施加复位信号，类似按下单片机复位按键
	RCC_APB1PeriphResetCmd(RCC_APB1Periph_I2C2, DISABLE);		// 释放复位信号，类似释放单片机复位按键
	
	// 3.开启 I2C2时钟 | 初始化 I2C
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C2, ENABLE);
	
	I2C_InitTypeDef I2C_InitStruct;
	I2C_InitStruct.I2C_ClockSpeed = 100000;				// 设置 I2C 速率为100kHz
	I2C_InitStruct.I2C_Mode = I2C_Mode_I2C;				// 设置 标准I2C模式
	I2C_InitStruct.I2C_DutyCycle = I2C_DutyCycle_2; 	// 设置占空比（高电平：低电平）
	I2C_InitStruct.I2C_OwnAddress1 = 0x00;				// 自身地址（不作为从设备时可设为 0x00）
	I2C_InitStruct.I2C_Ack = I2C_Ack_Enable;			// 启用应答
	I2C_InitStruct.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;	// 设置地址格式为7bit
	I2C_Init(I2C2, &I2C_InitStruct);	// 初始化 I2C2
	
	I2C_Cmd(I2C2, ENABLE);				// 使能 I2C2
}

/**
  * @brief 主机向OLED从机发送多个控制指令
  * @param Cmds: 待发送控制指令的字节数组
  * @param Length: 待发送控制指令的数量, 也就是Cmds字节数组的长度
  * @retval 0 表示正常完成通信 -1 表示寻址失败 -2 表示从机拒收数据
  */
static int8_t hOLED_I2CWriteCmds(const uint8_t *Cmds, uint8_t Length) {
    // 1.在开始通信(发送起始位)之前,要判断总线是否处于忙碌状态
    // - 若总线忙碌就等到总线不忙碌为止, 即等待总线进入空闲状态，标志位BUSY为RESET, 表示总线不再忙碌可以开始通信了
    while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BUSY) == SET);

    // 2.主机发送起始信号, 开始通信
    I2C_GenerateSTART(I2C2, ENABLE);

    // - 等待起始信号发送完毕，而后SB标志位为SET,可以发送寻址字节了
    while (I2C_GetFlagStatus(I2C2, I2C_FLAG_SB) == RESET);

    // 3.发送寻址字节
    I2C_SendData(I2C2, OLED_ADDRESS_WRITE);     // 主机发送寻址字节给从机

    // - 主机发送寻址字节后的处理逻辑(重点)
    while (1) {
        if (I2C_GetFlagStatus(I2C2, I2C_FLAG_ADDR) == SET) {
            // 从机地址匹配成功,ADDR标志位自动置1，需要手动把ADDR标志位置为0，若不清零后续发送过程无法开发
            // I2C_ClearFlag(I2C2, I2C_FLAG_ADDR);  这种软件清零的手段对ADDR标志位无效；ST公司规定 必须先读SR1寄存器, 再读SR2寄存器, 才能清零这个标志位
            I2C_ReadRegister(I2C2, I2C_Register_SR1);  		 // 读SR1寄存器
            I2C_ReadRegister(I2C2, I2C_Register_SR2);  		 // 读SR2寄存器
            break;
        }

        if (I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == SET) {
            // AF标志位置为1,说明从机无应答,寻址失败
            // 需要手动清零AF标志位,并处理通信失败
            I2C_ClearFlag(I2C2, I2C_FLAG_AF);
            // 主机发送停止信号,结束通信
            I2C_GenerateSTOP(I2C2, ENABLE);
            return -1;
        }
    }

    // 从机寻址成功, 开始发送下一个字节数据
    // 4.向OLED从机发送0x00, 表示OLED从机进入命令控制模式
    I2C_SendData(I2C2, 0x00);

    // 主机发送非寻址字节数据后的处理逻辑(重点)
    // 等待此1个字节的数据发送完成
    while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BTF) == RESET);

    // 再检查AF标志位是否置1, 判断从机是否应答
    if (I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == SET) {
        // 从机无应答, 拒收此数据
        I2C_GenerateSTOP(I2C2, ENABLE);     // 停止通信
        I2C_ClearFlag(I2C2, I2C_FLAG_AF);   // 手动清零AF标志位
        return -2;
    }


    // 5.循环得1个字节1个字节发送后续指令
    for (uint8_t i = 0; i < Length; i++) {
        I2C_SendData(I2C2, Cmds[i]);        // 发送1个字节指令到OLED

        // 主机每发送1个字节,就处理一次从机ACK
        // 主机发送非寻址字节数据后的处理逻辑(重点)
        // 等待此1个字节的数据发送完成
        while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BTF) == RESET);

        // 再检查AF标志位是否置1, 判断从机是否应答
        if (I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == SET) {
            // 从机无应答, 拒收此数据
            I2C_GenerateSTOP(I2C2, ENABLE);     // 停止通信
            I2C_ClearFlag(I2C2, I2C_FLAG_AF);   // 手动清零AF标志位
            return -2;
        }
    }

    // 所有指令都已经发出, 通信应该结束了
    // 主机发送停止信号, 通信结束
    I2C_GenerateSTOP(I2C2, ENABLE);
    return 0;
}

/**
  * @brief 开启屏幕，点亮全部像素并返回执行结果
  * @param  无
  * @retval  0 表示正常完成通信 -1 表示寻址失败 -2 表示从机拒收数据（同hOLED_I2CWriteCmds）
  */
int8_t hOLED_On(void) {
    // 开启屏幕的指令
    uint8_t OLED_ONCmds[] = {
        0x8D, 0x14, // 开启电荷泵
        0xAF,       // 打开屏幕
        0xA5        // 让屏幕全亮，点亮所有像素点
    };
    uint8_t SendResult = hOLED_I2CWriteCmds(OLED_ONCmds, sizeof(OLED_ONCmds));
    if(SendResult != 0){
        return SendResult;  // 发送指令给OLED的过程中,出现了各种错误
    }
    return 0;   // 发送成功
}

/**
  * @brief 彻底关闭OLED并返回执行结果
  * @param  无
  * @retval 0 表示正常完成通信 -1 表示寻址失败 -2 表示从机拒收数据（同hOLED_I2CWriteCmds）
  */
int8_t hOLED_Off(void) {
    // 关闭屏幕的指令
    uint8_t OLED_OFFCmds[] = {
        0xA4,       // 普通显示模式
        0xAE,       // 关闭 OLED 显示
        0x8D, 0x10  // 关闭电荷泵
    };
    uint8_t SendResult = hOLED_I2CWriteCmds(OLED_OFFCmds, sizeof(OLED_OFFCmds));
    if(SendResult != 0){
        return SendResult;  // 发送指令给OLED的过程中,出现了各种错误
    }
    return 0;   // 发送成功
}

/**
  * @brief 读取OLED开关状态利用指针传参获取这个状态
  * @param OLED_StatusPtr是一个外部变量的指针,传入函数的目的是获取当前OLED屏幕的点亮状态, 起着返回值的作用
  * @retval  无
  * @note 0表示查询成功，-1表示失败，-2表示OLED拒收
  */
int8_t hOLED_ReadStatus(uint8_t *OLED_StatusPtr) {
    // 1.在开始通信(发送起始位)之前,要判断总线是否处于忙碌状态.
    // 若总线忙碌就等到总线不忙碌为止
    while (I2C_GetFlagStatus(I2C2, I2C_FLAG_BUSY) == SET);

    // 当代码运行到这里标志位BUSY为RESET, 表示总线不再忙碌可以开始通信了
    // 2.主机发送起始信号, 开始通信
    I2C_GenerateSTART(I2C2, ENABLE);

    // 等待起始信号发送完毕
    while (I2C_GetFlagStatus(I2C2, I2C_FLAG_SB) == RESET);

    // 做法1: 任何时候想要发一个字节时,都手动清零AF标志位

    // 当代码运行到这里起始信号就发送完了,SB标志位为SET
    // 3.发送寻址字节
    I2C_SendData(I2C2, OLED_ADDRESS_READ);      // 表示向OLED从机写数据

    // 发送寻址字节后的处理逻辑
    while (1) {
        if (I2C_GetFlagStatus(I2C2, I2C_FLAG_ADDR) == SET) {
            // 手动清零ADDR标志位,若不清零后续发送过程无法开发
            // I2C_ClearFlag(I2C2, I2C_FLAG_ADDR);  这种软件清零的手段对ADDR标志位无效
            // ST公司规定 必须先读SR1寄存器, 再读SR2寄存器, 才能清零这个标志位
            I2C_ReadRegister(I2C2, I2C_Register_SR1);   // 读SR1寄存器
            I2C_ReadRegister(I2C2, I2C_Register_SR2);   // 读SR2寄存器
            // 处理从机地址匹配成功
            break;
        }

        if (I2C_GetFlagStatus(I2C2, I2C_FLAG_AF) == SET) {
            // 做法2: 一旦AF被置为1了, 处理中就手动清零AF标志位
            I2C_ClearFlag(I2C2, I2C_FLAG_AF);
            // 处理从机地址匹配失败
            I2C_GenerateSTOP(I2C2, ENABLE);     // 停止通信
            return -1;
        }
    }

    // 主机要读OLED状态字节,此时OLED会直接将状态字节数据发给主机
    // 主机会把这个状态字节数据存在自身的接收数据寄存器中
    while (I2C_GetFlagStatus(I2C2, I2C_FLAG_RXNE) == RESET);    // 等待接收数据寄存器非空

    // 4.主机读接收数据寄存器, 获取OLED状态字节
    uint8_t status_byte = I2C_ReceiveData(I2C2);
    *OLED_StatusPtr = ((status_byte >> 6) & 0x01);

    //5.主机发送NACK给从机表示不再接收数据,然后发送停止信号结束通信
    // 禁用主机ACK,主机收到从机数据后会发NACK表示不再接收数据了
    I2C_AcknowledgeConfig(I2C2, DISABLE);   
    I2C_GenerateSTOP(I2C2, ENABLE);     // 发送停止位, 结束通信
    return 0;
}
